Skip to content

feat: persist theme across page reloads and fix hydration flash#623

Open
pitah23 wants to merge 2 commits into
Arenax-gaming:mainfrom
pitah23:feat/theme-persistence
Open

feat: persist theme across page reloads and fix hydration flash#623
pitah23 wants to merge 2 commits into
Arenax-gaming:mainfrom
pitah23:feat/theme-persistence

Conversation

@pitah23

@pitah23 pitah23 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Closes #549


Summary

  • Persist theme to localStorage: wires storageKey="theme" into ThemeProvider so next-themes reads and writes the user's preference under the theme key; defaultTheme="system" and enableSystem ensure unset preferences fall back to the OS setting
  • Fix theme toggle for system mode: both ThemeToggle components previously used the raw theme value ("system") for the toggle decision; they now use resolvedTheme so a user on a dark-OS system correctly toggles to "light" (and vice-versa)
  • Eliminate hydration flash: both ThemeToggle components now render a stable placeholder button (no icon, no onClick) during SSR and before client hydration, preventing a flash of the wrong icon; the real button is swapped in after mounted = true
  • Sync ThemeSelector with persisted state: ThemeSelector previously highlighted the active mode based on the stale settings.mode prop; it now reads from useTheme() so the highlight always matches what next-themes actually has stored; mode selection calls both setTheme (persistence) and onUpdate (app-local state) together

Changes

File What changed
frontend/src/app/[locale]/layout.tsx Added storageKey="theme" to <ThemeProvider>
frontend/src/components/ThemeToggle.tsx Use resolvedTheme for toggle; add mounted guard
frontend/src/components/ui/ThemeToggle.tsx Same as above (navbar variant)
frontend/src/components/settings/ThemeSelector.tsx Read active mode from useTheme; call setTheme on selection; add mounted guard
frontend/src/__tests__/theme-persistence.test.tsx 27 new tests (see below)

Tests

All 27 tests pass; the 8 pre-existing failures in unrelated suites are unchanged.

ThemeProvider configuration (4)

  • Forwards storageKey="theme", defaultTheme="system", enableSystem, and all other props to NextThemesProvider unchanged

localStorage persistence (5)

  • Selected theme is stored under the "theme" key
  • Stored dark theme is restored after remount (stale settings prop is overridden)
  • Stored light theme is restored after remount
  • No preference in localStorage → active mode is "system"

ThemeToggle — ui/ThemeToggle (4 + flash)

  • lightdark, darklight
  • system + dark OS → toggles to light; system + light OS → toggles to dark
  • renderToStaticMarkup confirms SSR output is a placeholder with aria-label and no onclick (no flash)

ThemeToggle — components/ThemeToggle (3 + flash)

  • Same coverage for the layout/navbar variant

ThemeSelector (6)

  • Active mode comes from useTheme, not the stale settings.mode prop
  • Clicking a mode calls setTheme and onUpdate together for all three modes (light, dark, system)
  • Only the active mode button has the border-primary highlight; others do not

Cross-component sync (3)

  • Both ThemeToggle components read the same theme state and produce consistent setTheme calls
  • ThemeSelector highlights dark while ThemeToggle toggling away from dark — both agree on the persisted value
  • A mode chosen in ThemeSelector is immediately what ThemeToggle would then read

Test plan

  • Select Dark in Settings → Theme; reload the page — Dark mode is still active
  • Select Light → reload — Light mode persists
  • Select System — page follows OS preference; no stored preference means system default
  • Clear localStorage (theme key) and reload — falls back to system preference
  • Toggle the header sun/moon button from system+dark-OS — switches to Light
  • Open Settings → Theme; confirm the highlighted mode matches the header toggle
  • Open DevTools → Application → Local Storage: after any theme selection the theme key is present with the correct value
  • Hard-reload (Ctrl+Shift+R) — no flash of incorrect theme before the page finishes loading

pitah23 added 2 commits June 25, 2026 19:54
…to ProfileBio

- Define MAX_BIO_LENGTH (280) in src/lib/validations/profile.ts as the single
  source of truth, shared by the Zod schema and all UI components
- Update ProfileBio.tsx with a live character counter (currentLength / MAX),
  warning style (text-destructive) at ≥90% capacity, aria-live announcement,
  and inline Zod-driven error that blocks Save when the limit is exceeded
- Update profile/edit/page.tsx to import MAX_BIO_LENGTH from the shared
  location, removing the local 500-char constant that mismatched the server
- Add accessible label/id to the bio textarea in the edit page
- Fix broken import path in profile-edit.test.tsx and add useSearchParams mock
- Add profile-bio.test.tsx with 16 tests covering counter updates, 90%
  warning threshold, submission blocking, successful save, and constant/schema
  consistency
- Add storageKey="theme" to ThemeProvider so next-themes writes to and
  reads from the "theme" localStorage key (defaultTheme="system",
  enableSystem, and suppressHydrationWarning were already in place)
- Fix both ThemeToggle components to use resolvedTheme for the
  toggle decision, which correctly handles "system" mode (previously
  used the raw theme value, so system+dark-OS would not toggle to light)
- Add mounted guard to both ThemeToggle components: SSR renders a stable
  placeholder button (no Sun/Moon icons, no onClick) so users never see
  a flash of the wrong icon before client hydration
- Wire ThemeSelector to useTheme so its active-mode highlight reflects
  the persisted next-themes state instead of the stale settings prop;
  mode selection now calls setTheme (persistence) and onUpdate (app state)
  together; add same mounted guard for hydration safety

Tests (27) in theme-persistence.test.tsx:
- ThemeProvider forwards storageKey, defaultTheme, enableSystem correctly
- localStorage: selected theme stored under "theme", restored on remount,
  falls back to system when no preference is present
- ThemeToggle (both): light↔dark toggle, system+OS-pref toggle,
  SSR output verified via renderToStaticMarkup (placeholder, no onclick)
- ThemeSelector: active mode from useTheme not settings, setTheme+onUpdate
  called together, correct highlight for all three modes
- Cross-component sync: ThemeToggle and ThemeSelector reflect same state
@pitah23 pitah23 requested a review from anonfedora as a code owner June 25, 2026 21:42
@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown

@pitah23 is attempting to deploy a commit to the paul joseph's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Jun 25, 2026

Copy link
Copy Markdown

@pitah23 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[frontend] - Theme selector does not persist chosen theme after page reload

1 participant